ElixirのFailoverとTakeover
アプリケーションに何らかの障害が発生した場合に、アプリケーションを別のノードで立ち上げ直す(Failover)と、復旧した時にアプリケーションを元のノードに切り戻す(Takeover)機能があれば信頼性が向上します。
今回はFailoverとTakeoverをElixirのアプリケーションで試してみます。
概要
簡単にFailoverとTakeoverの説明をします。
以下はノードA、 ノードB、 ノードCでクラスタを組んでいる場合のFailoverとTakeoverの例です。
通常時はノードAで起動
ノードAで障害が発生した場合にノードBでアプリケーションが立ち上がります。これがFailoverです。
ノードBでも障害が発生した場合、今度はノードCで起動します。
そして、ノードAが復旧すると別のノードで起動していたアプリケーションがノードAで起動し直します。これがTakeoverです。
クラスタの構築
通常時にアプリケーションを稼働させるマスターノードと障害時にアプリケーションを切り替えるスレーブノードを設定します。
今回はサーバ2台での構成で試します。サーバAではマスターノード、サーバBではスレーブノードの2ノードが待機系として起動します。
ノード毎に設定ファイルを用意します。
以下はmasterノードの設定ファイルです。
config/master.config
[{kernel, [{distributed, [{wikipedy, 5000, ['[email protected]', {'[email protected]', '[email protected]'}]}]}, {sync_nodes_mandatory, ['[email protected]', '[email protected]']}, {sync_nodes_timeout, 30000} ]}].
{distributed, [{wikipedy, 5000, ['[email protected]', {'[email protected]', '[email protected]'}]}]}
この行は以下の内容を指定しています。
- アプリケーション名
- ノードがダウンしたと判定するまでのミリ秒
- クラスタを構築するノードをそれぞれ指定しています
(クラスタを構築するノードの指定では優先度も指定しています。この設定値では、優先度が一番高いノードが[email protected]
でその次に [email protected]
もしくは [email protected]
が続きます)
sync_nodes_mandatoryとsync_nodes_timeoutはそれぞれ以下の意味です。
sync_nodes_mandatory | ここに指定したノードが起動するまでアプリケーションの起動を待ちます |
sync_nodes_timeout | ここで指定したタイムアウト値まで指定したノードが起動してこないとアプリケーションの起動を諦めてクラッシュします |
同様にslaveノードの設定ファイルも用意します。2ノードそれぞれのファイルが必要です。
config/slave-a.config
[{kernel, [{distributed, [{wikipedy, 5000, ['[email protected]', {'[email protected]', '[email protected]'}]}]}, {sync_nodes_mandatory, ['[email protected]', '[email protected]']}, {sync_nodes_timeout, 30000} ]}].
config/slave-b.config
[{kernel, [{distributed, [{wikipedy, 5000, ['[email protected]', {'[email protected]', '[email protected]'}]}]}, {sync_nodes_mandatory, ['[email protected]', '[email protected]']}, {sync_nodes_timeout, 30000} ]}].
アプリケーションの起動タイプ
アプリケーション起動時に渡されるtype
の値が通常起動時、フェイルオーバー時、テイクオーバー時で異なります。
引数 | 内容 |
---|---|
:normal |
通常の起動に渡される |
{:faliover, node} |
アプリケーションがフェイルオーバーした時に渡される |
パターンマッチで引数を判定しそれぞれの場合の特有の処理を入れることができます。
ここではわかりやすくログ出力をそれぞれのパターン毎に入れます。
lib/wikipedy.ex
defmodule Wikipedy do use Application require Logger def start(type, _args) do import Supervisor.Spec children = [ worker(Wikipedy.Server, []) ] case type do :normal -> Logger.info """ ################################################## Application is started on #{node} ################################################## """ {:takeover, old_node} -> Logger.info """ ################################################### #{node} is taking over #{old_node} ################################################### """ {:failover, old_node} -> Logger.info """ ######################################################### #{old_node} is failing over to #{node} ######################################################### """ end opts = [strategy: :one_for_one, name: {:global, Wikipedy.Supervisor}] Supervisor.start_link(children, opts) end end
あとはアプリケーションロジックの実装です。
なんでも良いのですが、ノードの切り替えが分かりやすいように一定間隔で標準出力するプログラムにします。
Wikipediaの「本日の出来事」を取得してその中の1件をランダムに取得して表示します。
lib/wikipedy/server.ex
defmodule Wikipedy.Server do use GenServer # API def start_link do GenServer.start_link(__MODULE__, [], [name: {:global, __MODULE__}]) end def init([]) do :random.seed(:os.timestamp) my_loop {:ok, []} end def my_loop do # 1.5秒に1回、「本日の出来事」からランダムな1剣を取得して標準出力する関数を呼び出します Process.send_after(self, :today_topic, 1_500) end def handle_info(:today_topic, topics) do topics = case Enum.empty?(topics) do true -> case Wikipedy.TopicFetcher.topics do {:ok, list} -> list {:error, reason} -> IO.puts(reason) topics end false -> topics end IO.puts(Enum.random(topics)) my_loop {:noreply, topics } end end
Wikipediaからスクレイピングで「本日の出来事」を取得するプログラムです。
lib/wikipedy/topic_fetcher.ex
defmodule Wikipedy.TopicFetcher do def topics do {{_,m,d}, _} = :calendar.local_time case HTTPoison.get(URI.encode("https://ja.wikipedia.org/wiki/#{m}月#{d}日")) do {:ok, %HTTPoison.Response{body: body}} -> topic_elms = String.split(body, "</h2>") |> Enum.at(2) topics = Floki.find(topic_elms, "mw-content-text, ul") |> Enum.at(0) |> Floki.text |> String.split("。") {:ok, topics} {:error, reason} -> {:error, reason} end end end
今回使用するライブラリをmix.exsの依存ライブラリに追加します。
mix.exs
defp deps do [ {:httpoison, "~> 0.9.0"}, {:floki, "~> 0.10.0"}, {:distillery, ">= 0.8.0", warn_missing: false}, ] end
distillery
は、ビルドで使用するために追加しています。
ビルド
今回はEC2上で試します。セキュリティグループの設定が必要になりますので、以下のページを参考に設定してください。
Elixirのノード接続とメッセージ通信
それではアプリケーションのビルドを行います。
まず初期化コマンドを実行して設定ファイルを作成します。
$ mix release.init
次のコマンドでビルドします。
$ mix release
_build/prod/rel/アプリケーション名/release/0.1.0
にtarファイルが出来ます。
このファイルをサーバのアプリケーション実行ディレクトリに解凍してください。
VMパラメータ設定ファイル
解凍したアプリケーション配下のreleases/0.1.0
ディレクトリにvm.argsファイルがあります。
このファイルにアプリケーション実行時のパラメータを指定します。
vm.args
## 実行するノード名を指定します -name [email protected] ## ここに指定するクッキーの値は全ノードで共通の値にします -setcookie secret ## クラスタ構築するために作成した設定ファイルのパスを指定します。それぞれのノード毎にファイルを指定します -config /home/ec2-user/deploy/master.config ## AWSのセキュリティグループに設定したポート番号を指定します -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155
同じようにslave-a、slave-bのvm.argsにも設定します
実行
それでは、EC2上で実行してみます。
masterノードとslaveノードがそれぞれ別サーバで動きます。slaveノードは二つ起動します。
REPLを起動してみましょう(3ノード起動します)。
$ bin/wikipedy console
うまくFailover, Takeover出来ました。
処理は以下の流れで進みます。
順番 | 内容 |
---|---|
1 | 最初、masterノードでアプリケーションが起動します |
2 | masterノードのREPLを強制終了すると、一つ目のslaveノード上でアプリケーションがが起動します(Failover) |
3 | slaveノードのREPLも強制終了すると、二つ目のslaveノード上でアプリケーションが起動します(Failover) |
4 | masterノードのREPLを再度起動すると、アプリケーションがmasterノードで起動し直します(Takeover) |
Failover、Takeoverの機能を実現するためのミドルウェアはすでに多くあると思いますが、それが言語のランタイムに含まれているのはすごいですね。
興味がある方は是非試してみてください。
参考情報
The little Elixir & OTP Guidebook
プログラミングElixir
すごいErlangゆかいに学ぼう!